﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using NDesk.Options;

namespace Md5Verifier
{
  class Program
  {
    static string _path = string.Empty;
    private static bool _verbose;
    private static bool _absoulte;
    private static bool _help;
    private static string _outPutPath;
    private static string _baseFolderPath;
    private static bool _check;
    private static bool _errors;
    private static readonly Encoding _encoding = Encoding.GetEncoding(1251);

    static void Main(string[] args)
    {
      var options = new OptionSet
                {
                  { "p|path=", "the {PATH} for file/folder to process.",
                    s => _path = s },
                  { "o|out=", "the {PATH} for output file.",
                    s => _outPutPath = s },
                  { "c|check=", "verifies MD5 sums using MD5 checksum file.",
                    s => { if (s != null) { _check = true; _path = s; } } },
                  { "ve|errors", "if set, ouputs errors if file processed with error.",
                    s => { if (s != null) _errors = true; } },
                  { "v|verbose", "if set, outputs info about every file being processed.",
                    s => { if (s != null) _verbose = true; } },
                  { "a|absolute", "if set, uses the absolute paths in the output.",
                    s => { if (s != null) _absoulte = true; } },
                  { "b|basefolder=", "the {PATH} to the base folder while checking. If not set, the folder of file with MD5 checksums is used.",
                    s => { if (s != null) _baseFolderPath = s; } },
                  { "h|?|help", "shows help. You are welcome.",
                    s => { if (s != null) _help = true; } },
                };

      try
      {
        options.Parse(args);
        if (_help)
        {
          WriteHelp(options);
          return;
        }
        if (string.IsNullOrEmpty(_path))
        {
          Console.WriteLine("Path can not be empty.");
          WriteHelp(options);
          return;
        }
        if (_check)
        {
          CheckFileMd5();
          return;
        }
        if (string.IsNullOrEmpty(_outPutPath))
        {
          Console.WriteLine("Output file path can not be empty.");
          WriteHelp(options);
          return;
        }
        if (Directory.Exists(_path))
        {
          GetFolderMd5();
        }
        else if (File.Exists(_path))
        {
          var fi = new FileInfo(_path);
          using (var writer = File.CreateText(_outPutPath))
            writer.Write("{0} *{1}", fi.Name, GetMd5HashFromFile(fi.FullName));
        }
        else
        {
          Console.WriteLine("There is no such file or folder: '{0}'", _path);
          WriteHelp(options);
        }
      }
      catch (OptionException e)
      {
        Console.Write("Md5Verifier error: ");
        Console.WriteLine(e.Message);
        return;
      }

    }

    private static void WriteHelp(OptionSet options)
    {
      options.WriteOptionDescriptions(Console.Out);
      Console.WriteLine(Environment.NewLine + @"Warning!!! Do not use '\' at the end for folder paths.");
      Console.WriteLine(@"While the path 'D:\temp\' will not work, the path 'D:\temp' works perfect.");
    }

    private static void CheckFileMd5()
    {
      if (!File.Exists(_path))
      {
        Console.WriteLine("There is no such file: '{0}'", _path);
        return;
      }
      var md5FileText = File.ReadAllLines(_path, _encoding);
      var readErrorsCount = 0;
      var notFoundCount = 0;
      var crcErrorsCount = 0;
      var linesCount = 0;
      foreach (var line in md5FileText)
      {
        linesCount++;
        var splitted = line.Split(new[] { "*" }, StringSplitOptions.RemoveEmptyEntries);
        if (splitted.Length < 2)
          Console.WriteLine("Error parsing MD5 checksum. Line {0}", linesCount);
        var filePath = splitted[1];
        if (!string.IsNullOrEmpty(_baseFolderPath))
        {
          // use base path from command line args
          filePath = Path.Combine(_baseFolderPath, filePath.TrimStart(new[] { '\\' }));
        }
        else
        {
          // use base path from MD5 checksums file
          var basePath = new FileInfo(_path).DirectoryName;
          filePath = Path.Combine(basePath, filePath.TrimStart(new[] { '\\' }));
        }
        var checkSum = splitted[0];
        if (!File.Exists(filePath))
        {
          notFoundCount++;
          if (_verbose)
            Console.WriteLine("File not found: '{0}'", filePath);
          continue;
        }
        try
        {
          using (File.OpenRead(filePath))
          {
            var actualCheckSum = GetMd5HashFromFile(filePath);
            if (checkSum.Trim() != actualCheckSum)
            {
              crcErrorsCount++;
              if (_verbose)
                Console.WriteLine("Wrong CRC: '{0}'", filePath);
            }
          }
        }
        catch (Exception ex)
        {
          readErrorsCount++;
          if (_verbose)
            Console.Error.WriteLine(ex);
        }
      }
      Console.WriteLine("Errors: {0}", crcErrorsCount + readErrorsCount + notFoundCount);
      var okFiles = linesCount - (crcErrorsCount + readErrorsCount + notFoundCount);
      Console.WriteLine("OK: {0}, not found: {1}, read error: {2}, wrong checksum: {3}",
        okFiles, notFoundCount, readErrorsCount, crcErrorsCount);
    }

    private static void GetFolderMd5()
    {
      try
      {
        var di = new DirectoryInfo(_path);
        var startTime = DateTime.Now;
        Console.WriteLine("Scanning for files... This could take some time.");
        var files = WalkFiles(di.FullName, "*.*").ToList();
        Console.WriteLine("Found {0} files.", files.Count);
        var errorsCount = 0;
        using (var writer = new StreamWriter(_outPutPath, false, _encoding))
        {
          var i = 0;
          foreach (var file in files)
          {
            i++;
            try
            {
              if (!_verbose)
              {
                var progress = (int)(i * 100.0 / files.Count);
                Console.Write(String.Format("Percent complete {0}%\r", progress));
              }
              else
              {
                Console.WriteLine("Processing '{0}'", file);
              }
              var fi = new FileInfo(file);
              var outputFileInfo = new FileInfo(_outPutPath);
              if (fi.FullName == outputFileInfo.FullName)
              {
                Console.WriteLine("Output file '{0}' is skipped.", file);
                continue;
              }
              var pathToWrite = _absoulte ? fi.FullName : fi.FullName.Replace(di.FullName, "");
              writer.WriteLine("{0} *{1}", GetMd5HashFromFile(fi.FullName), pathToWrite);
            }
            catch (Exception ex)
            {
              errorsCount++;
              if (_errors)
                Console.WriteLine("Read error: {0}", ex.Message);
            }
          }
        }
        var endTime = DateTime.Now;
        if (!_verbose) Console.Write("                         "); // clean %
        Console.WriteLine("{0}Total files processed: {1}", Environment.NewLine, files.Count);
        Console.WriteLine("Errors: {0}", errorsCount);
        Console.WriteLine("Time demanded for MD5 sums calculation: {0}", endTime - startTime);
      }
      catch (Exception ex)
      {
        Console.Error.WriteLine(ex.Message);
      }
    }

    private static IEnumerable<string> WalkFiles(string path, string filter)
    {
      var pending = new Queue<string>();
      pending.Enqueue(path);
      while (pending.Count > 0)
      {
        path = pending.Dequeue();
        var tmp = Directory.GetFiles(path, filter);
        foreach (var t in tmp)
          yield return t;
        tmp = Directory.GetDirectories(path);
        foreach (var t in tmp)
          pending.Enqueue(t);
      }
    }

    private static string GetMd5HashFromFile(string fileName)
    {
      byte[] retVal;
      using (var fs = File.OpenRead(fileName))
      {
        MD5 md5 = new MD5CryptoServiceProvider();
        retVal = md5.ComputeHash(fs);
        fs.Close();
      }

      var sb = new StringBuilder();
      for (var i = 0; i < retVal.Length; i++)
      {
        sb.Append(retVal[i].ToString("x2"));
      }
      return sb.ToString();
    }

  }

}
